package cn.lijiajia3515.cairo.auth.modules.role;

import cn.lijiajia3515.cairo.auth.domain.mongo.AuthMongoConstants;
import cn.lijiajia3515.cairo.auth.domain.mongo.ResourceMongo;
import cn.lijiajia3515.cairo.auth.domain.mongo.RoleMongo;
import cn.lijiajia3515.cairo.auth.domain.mongo.AccountMongo;
import cn.lijiajia3515.cairo.auth.modules.AuthExtensionConstants;
import cn.lijiajia3515.cairo.auth.modules.resource.ResourceConverter;
import cn.lijiajia3515.cairo.auth.modules.resource.ResourceTreeNode;
import cn.lijiajia3515.cairo.auth.modules.account.Account;
import cn.lijiajia3515.cairo.auth.modules.account.AccountRClient;
import cn.lijiajia3515.cairo.common.BaseService;
import cn.lijiajia3515.cairo.core.CoreConstants;
import cn.lijiajia3515.cairo.core.exception.BusinessException;
import cn.lijiajia3515.cairo.core.exception.UnknownBusinessException;
import cn.lijiajia3515.cairo.core.page.Page;
import cn.lijiajia3515.cairo.security.SecurityConstants;
import com.mongodb.BasicDBObject;
import com.mongodb.client.result.UpdateResult;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.aggregation.Aggregation;
import org.springframework.data.mongodb.core.aggregation.Field;
import org.springframework.data.mongodb.core.aggregation.Fields;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;

import javax.validation.constraints.NotNull;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * 服务 - 角色
 */
@Slf4j
@Service
public class RoleService extends BaseService {

	public final Collection<String> UNMODIFIABLE_ROLE_CODES = Collections.singletonList(SecurityConstants.Role.ADMIN);

	private final MongoTemplate mongoTemplate;

	public RoleService(AccountRClient userRClient, MongoTemplate mongoTemplate) {
		super(userRClient);
		this.mongoTemplate = mongoTemplate;
	}

	/**
	 * 保存
	 */
	@Transactional(rollbackFor = Exception.class)
	RoleV2 save(String appId, SaveRoleParam param) {
		validUnModifiableRole(Collections.singleton(param.getId()));
		RoleMongo role = RoleMongo.builder()
			.client(appId)
			.id(Optional.ofNullable(param.getId()).orElse(CoreConstants.SNOWFLAKE.nextIdStr()))
			.name(param.getName())
			.resources(param.getResources())
			.build();
		RoleMongo insert = mongoTemplate.insert(role, AuthMongoConstants.Collection.ROLE);
		log.debug("[role][insert] -> {}", insert);
		return findByCode(appId, insert.getId()).orElseThrow(() -> new UnknownBusinessException("code not found"));
	}

	/**
	 * 修改
	 *
	 * @param appId appId
	 * @param param  param
	 * @return role
	 */
	@Transactional(rollbackFor = Exception.class)
	Optional<RoleV2> modify(String appId, @Validated ModifyRoleParam param) {
		validUnModifiableRole(Collections.singleton(param.getId()));
		Query query = Query.query(Criteria
			.where(RoleMongo.FIELD.CLIENT).is(appId)
			.and(RoleMongo.FIELD.ID).is(param.getId())
		);
		Update update = new Update();
		Optional.ofNullable(param.getName()).ifPresent(name -> update.set(RoleMongo.FIELD.NAME, name));
		Optional.ofNullable(param.getResources()).ifPresent(resourceIds -> update.set(RoleMongo.FIELD.RESOURCES, resourceIds));
		UpdateResult result = mongoTemplate.updateFirst(query, update, RoleMongo.class, AuthMongoConstants.Collection.ROLE);
		log.debug("[role][modify] -> {}", result);
		return findByCode(appId, param.getId());
	}

	/**
	 * 删除
	 *
	 * @param appId appId
	 * @param param  param
	 * @return delete role data
	 */
	@Transactional(rollbackFor = Exception.class)
	List<RoleV2> delete(@NotNull String appId, @Validated DeleteRoleParam param) {
		validUnModifiableRole(param.getIds());
		Criteria criteria = Criteria.where(RoleMongo.FIELD.CLIENT).is(appId);
		Optional.of(param.getIds()).filter(x -> !x.isEmpty()).ifPresent(x -> criteria.and(RoleMongo.FIELD.ID).in(x));
		Query query = Query.query(criteria);
		if (existsHasUser(appId, new HashSet<>(param.getIds()))) {
			throw new BusinessException("删除角色中已绑定用户", RoleBusiness.NotDelete);
		}

		List<RoleV2> deleteRoles = mongoTemplate.findAllAndRemove(query, RoleMongo.class, AuthMongoConstants.Collection.ROLE)
			.stream()
			.map(x -> RoleConverter.convert(x, findResources(x)))
			.collect(Collectors.toList());

		log.debug("[role][delete] -> {}", deleteRoles);
		return deleteRoles;

	}


	/**
	 * find
	 *
	 * @param appId appId
	 * @param param  param
	 * @return role page
	 */
	List<Role> find(@NotNull String appId, @Validated FindRoleParam param) {
		Criteria criteria = buildFindParamCriteria(appId, param);
		Query query = Query.query(criteria);
		query.with(
			Sort.by(
				Sort.Order.desc(RoleMongo.FIELD.META.SORT),

				Sort.Order.desc(RoleMongo.FIELD.META.LAST_MODIFIED.AT),
				Sort.Order.desc(RoleMongo.FIELD.META.CREATED.AT),
				Sort.Order.desc(RoleMongo.FIELD.ID),
				Sort.Order.desc(RoleMongo.FIELD._ID)
			)
		);

		final List<RoleMongo> rms = mongoTemplate.find(query, RoleMongo.class, AuthMongoConstants.Collection.ROLE);
		return find(appId, rms, param.getExtension());
	}


	/**
	 * find page
	 *
	 * @param appId appId
	 * @param param  param
	 * @return role page
	 */
	Page<Role> findPage(@NotNull String appId, @Validated FindRoleParam param) {
		Criteria criteria = buildFindParamCriteria(appId, param);
		Query query = Query.query(criteria);

		long total = mongoTemplate.count(query, RoleMongo.class, AuthMongoConstants.Collection.ROLE);

		query.with(param.pageable());
		List<RoleMongo> rms = mongoTemplate.find(query, RoleMongo.class, AuthMongoConstants.Collection.ROLE);
		final List<Role> rs = find(appId, rms, param.getExtension());
		return new Page<>(param, rs, total);
	}

	public void test() {

	}

	List<Role> find(String appId, List<RoleMongo> ms, Map<String, String> extensionMap) {
		RoleExtension extension = Optional.ofNullable(extensionMap.get(AuthExtensionConstants.ROLE)).map(RoleExtension::valueOf).orElse(RoleExtension.ALL);
		final Map<String, Integer> userNumMap = Optional.of(extension.fields())
			.filter(x -> x.contains(RoleField.USER_NUM))
			.map(x -> ms.stream().map(RoleMongo::getId).collect(Collectors.toSet()))
			.map(x -> {
				final Field clientKey = Fields.field(AccountMongo.FIELD.ROLE.client(appId));
				return mongoTemplate.aggregate(Aggregation.newAggregation(
						Aggregation.match(Criteria.where(clientKey.getTarget()).elemMatch(new Criteria().in(x))),
						Aggregation.project(Fields.from(clientKey)),
						Aggregation.unwind(clientKey.getName()),
						Aggregation.group(clientKey.getName()).count().as("Num"),
						Aggregation.sort(Sort.by(Sort.Order.desc(clientKey.getName())))
					), AuthMongoConstants.Collection.ACCOUNT, BasicDBObject.class).getMappedResults().stream()
					.collect(Collectors.toMap(z -> z.getString("_id"), z -> z.getInt("Num")));
			})
			.orElse(Collections.emptyMap());

		final Map<String, Account> accountMap = Optional.of(extension.fields())
			.filter(x -> x.contains(RoleField.METADATA))
			.map(x -> getAccountMap(ms.stream().map(RoleMongo::getMeta).collect(Collectors.toList()), extensionMap))
			.orElse(Collections.emptyMap());

		return ms.stream()
			.map(x -> RoleConverter.convert(x, userNumMap, accountMap, extension))
			.collect(Collectors.toList());
	}

	Criteria buildFindParamCriteria(@NotNull String appId, @NotNull FindRoleParam param) {
		Criteria criteria = Criteria.where(RoleMongo.FIELD.CLIENT).is(appId);
		Optional.ofNullable(param.getKeyword()).filter(kw -> !kw.isEmpty()).ifPresent(kw -> criteria.and(RoleMongo.FIELD.NAME).regex(kw));
		return criteria;
	}

	Optional<RoleV2> findByCode(String appId, String code) {
		Query query = Query.query(Criteria.where(RoleMongo.FIELD.CLIENT).is(appId).and(RoleMongo.FIELD.ID).is(code));
		RoleMongo role = mongoTemplate.findOne(query, RoleMongo.class, AuthMongoConstants.Collection.ROLE);
		return Optional.ofNullable(role).map(x -> RoleConverter.convert(x, findResources(role)));
	}

	Map<String, ResourceTreeNode> findResources(RoleMongo role) {
		return Optional.ofNullable(role)
			.map(RoleMongo::getResources)
			.filter(x -> !x.isEmpty())
			.map(x ->
				mongoTemplate.find(
					Query.query(Criteria
						.where(ResourceMongo.FIELD.CLIENT).is(role.getClient())
						.and(ResourceMongo.FIELD._ID).in(x)),
					ResourceMongo.class,
					AuthMongoConstants.Collection.RESOURCE)
			).stream().flatMap(Collection::stream)
			.map(ResourceConverter::convert)
			.collect(Collectors.toMap(ResourceTreeNode::getId, x -> x));
	}

	void validUnModifiableRole(Collection<String> codes) {
		List<String> unModifiableRoleCodes = codes.stream().filter(UNMODIFIABLE_ROLE_CODES::contains).collect(Collectors.toList());
		if (!unModifiableRoleCodes.isEmpty())
			throw new BusinessException(String.format("无权修改编辑 %s 角色", codes), RoleBusiness.NotModify);
	}

	@Transactional(propagation = Propagation.SUPPORTS, rollbackFor = Exception.class)
	boolean existsHasUser(String appId, Set<String> roles) {
		final Criteria criteria = Criteria.where(AccountMongo.FIELD.META.DELETED).is(AuthMongoConstants.NO_DELETED_VALUE)
			.and(AccountMongo.FIELD.ROLE.client(appId)).elemMatch(new Criteria().in(roles));
		return mongoTemplate.exists(Query.query(criteria), AuthMongoConstants.Collection.ACCOUNT);
	}

	@Data
	@Accessors(chain = true)
	@NoArgsConstructor
	@AllArgsConstructor
	@Builder
	public static class RoleCountAgg {
		private String id;

		private Integer num;
	}

}
